/******************************************************************************
 * \brief	进程间网络套接字通信
 * \details	用来模拟两个嵌入式设备间的串口通信；有点区别的是套接字是可靠通信，串口是
 *			不可靠通信，套接字可以不使用校验，串口必须要使用校验
 * \note	File format: UTF-8，中文编码：UTF-8
 * \author	将狼才鲸
 * \date	2023-03-21
 ******************************************************************************/

/*********************************** 头文件 ***********************************/
#include "util_errno.h"
#include "util_print.h"
#include "dev_socket.h"
#include <sys/socket.h>	/* socket AF_UNIX SOCK_STREAM sockaddr bind accept connect */
#include <sys/un.h>		/* sockaddr_un */
#include <stdio.h>		/* perror printf */
#include <string.h>		/* strcpy */
#include <unistd.h>		/* unlink read write close */
#include <fcntl.h>		/* fcntl */

/*********************************** 宏定义 ***********************************/
#define SOCKET_FILE "/tmp/linux.socket"	/* 本机通信使用的虚拟内存文件 */

/******************************** 模块私有变量 ********************************/
static int listen_fd = 0;
static int server_connect_fd = 0;
static int client_connect_fd = 0;

/********************************** 接口函数 **********************************/
/**
 * \brief	服务器端套接字初始化
 */
int socket_server_init(void)
{
	struct sockaddr_un srv_addr;	/* 服务器域套接字地址 */
	struct sockaddr_un clt_addr; 
	int ret;

	/* 打开一个套接字，地址族协议：本地IPC通信（非ipv4或ipv4），
	   套接字类型：流，使用的协议：无（非TCP或UDP）；
	   调用socket时可以使用SOCK_STREAM | SOCK_NONBLOCK来设置非阻塞，但是这样会让accept失败；
	   所以最好是服务器端在accept后，客户端在connect后使用使用fcntl函数来设置；
	   也可以使用accept4函数来设置非阻塞；
	   非阻塞后，read或者recv的时候没读到数据时函数立即返回； */
	listen_fd = socket(AF_UNIX, SOCK_STREAM, 0);
	if (listen_fd < 0) {
		pr_err("server open socket fail!\n");
		return -ENOENT;
	}

	srv_addr.sun_family = AF_UNIX;	/* 本机IPC通信 */
	strcpy(srv_addr.sun_path, SOCKET_FILE);	/* 使用虚拟内存文件 */

	unlink(SOCKET_FILE);	/* 绑定前先取消本地内存文件链接，防止绑定报错 */
	/* 绑定套接字和协议，使用本地内存文件 */
	ret = bind(listen_fd, (struct sockaddr*)&srv_addr, sizeof(srv_addr));
	if (ret < 0) {
		pr_err("bind error!\n");
		goto exit1;
	}

#define LISTEN_MAX_NUM 8	/* 允许监听但还未连接的链路总数 */
	/* 监听链路 */
	ret = listen(listen_fd, LISTEN_MAX_NUM);
	if (ret < 0) {
		pr_err("listen error!\n");
		goto exit1;
	}

	pr_info("waitting for client socket connect...\n");

	int clt_addr_len = sizeof(clt_addr); 
	/* 等待客户端握手连接，如果客户端一直没连接，则会阻塞在这里 */
	server_connect_fd = accept(listen_fd, (struct sockaddr *)&clt_addr, &clt_addr_len);
	if (ret < 0) {
		pr_err("accept error!\n");
		goto exit1;
	}
	pr_info("client connect server pass\n");

	/* 使用非阻塞的读；如果想要一定读取到指定长度的内容，则要用select或者pselect */
	int flags = fcntl(server_connect_fd, F_GETFL, 0);
	fcntl(server_connect_fd, F_SETFL, flags | O_NONBLOCK);	/* 设置非阻塞 */

	return 0;

exit1:
	unlink(SOCKET_FILE);
	/* 关闭套接字 */
	close(listen_fd);

	return -EAGAIN;
}

/**
 * \brief	服务器端退出套接字
 */
int socket_server_exit(void)
{
	/* 关闭连接 */
	close(server_connect_fd);

	unlink(SOCKET_FILE);
	/* 关闭套接字 */
	close(listen_fd);

	return 0;
}

/**
 * \brief	服务器端发送数据
 * \note	也可以使用send函数
 */
int socket_server_send(char *data, int len)
{
	return write(server_connect_fd, data, len);
}

/**
 * \brief	服务器端接收数据
 * \note	也可以使用recv函数
 * \param	data:	存放数据的地址
 * \param	size:	想要读的长度（只要读缓存空了，没读满也会立即返回）
 * \return	返回实际读到的长度，可以是0，或者返回错误码
 */
int socket_server_recv(char *data, int size)
{
	/* note: 可以加入select或者pselect做成一定要读满指定的长度，否则干脆不读 */
	return read(server_connect_fd, data, size);
}

/**
 * \brief	客户端套接字初始化
 */
int socket_client_init(void)
{
	struct sockaddr_un srv_addr;	/* 服务器域套接字地址 */
	struct sockaddr_un clt_addr; 
	int ret;

	/* 打开一个套接字，地址族协议：本地IPC通信（非ipv4或ipv4），
	   套接字类型：流，使用的协议：无（非TCP或UDP） */
	client_connect_fd = socket(AF_UNIX, SOCK_STREAM, 0);
	if (client_connect_fd < 0) {
		pr_err("client open socket fail!\n");
		return -ENOENT;
	}

	srv_addr.sun_family = AF_UNIX;	/* 本机IPC通信 */
	strcpy(srv_addr.sun_path, SOCKET_FILE);	/* 使用虚拟内存文件 */

	/* 握手和连接服务器端，非阻塞，如果连不上服务器则直接报错 */
	ret = connect(client_connect_fd, (struct sockaddr*)&srv_addr, sizeof(srv_addr));
	if (ret < 0) {
		pr_err("connect error!\n");
		goto exit;
	}

	/* 使用非阻塞的读；如果想要一定读取到指定长度的内容，则要用select或者pselect */
	int flags = fcntl(client_connect_fd, F_GETFL, 0);
	fcntl(client_connect_fd, F_SETFL, flags | O_NONBLOCK);	/* 设置非阻塞 */

	return 0;

exit:
	/* 关闭套接字 */
	close(client_connect_fd);

	return -EAGAIN;
}

/**
 * \brief	客户端退出套接字
 */
int socket_client_exit(void)
{
	/* 关闭套接字 */
	close(client_connect_fd);

	return 0;
}

/**
 * \brief	客户端发送数据
 * \note	也可以使用send函数
 */
int socket_client_send(char *data, int len)
{
	return write(client_connect_fd, data, len);
}

/**
 * \brief	客户端接收数据
 * \note	也可以使用recv函数
 * \param	data:	存放数据的地址
 * \param	size:	想要读的长度（只要读缓存空了，没读满也会立即返回）
 * \return	返回实际读到的长度，可以是0，或者返回错误码
 */
int socket_client_recv(char *data, int size)
{
	/* >0表示读到的数据长度，0代表未读到数据，<1表示读数据出错 */
	return read(client_connect_fd, data, size);
}

/*********************************** 文件尾 ***********************************/
